#include <string.h>
#include <glib/gprintf.h>
#include "gsm-model.h"
#include "gsm-watcher-source.h"
#include "gsm-marshals.h"

enum {
	PROP_0,
	PROP_UPDATE_RATE,
	PROP_PAUSE
};

enum {
	WATCHER_ADD,
	WATCHER_UPDATE,
	WATCHER_REMOVE,
	WATCHER_ERROR,
	FRIEND_ADD,
	FRIEND_UPDATE,
	FRIEND_REMOVE,
	FRIEND_ONLINE,
	LAST_SIGNAL
};

static void gsm_model_get_property (GObject* object, guint prop_id,
		GValue* value, GParamSpec* pspec);
static void gsm_model_set_property (GObject* object, guint prop_id,
		const GValue* value, GParamSpec* pspec);
static void gsm_model_dispose (GObject *object);

static void gsm_model_remove_server_real (GsmModel *model, GsmWatcher *watcher);

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE (GsmModel, gsm_model, G_TYPE_OBJECT)

static void
gsm_model_class_init (GsmModelClass *klass)
{
	GObjectClass* gobject_class = (GObjectClass *) klass;
	gobject_class->set_property = gsm_model_set_property;
	gobject_class->get_property = gsm_model_get_property;
	gobject_class->dispose = gsm_model_dispose;
	
	g_object_class_install_property (gobject_class, PROP_UPDATE_RATE,
			g_param_spec_long ("update-rate", "Update rate", "Update rate",
					500, G_MAXLONG, 3000,
					G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT));
	
	g_object_class_install_property (gobject_class, PROP_PAUSE,
			g_param_spec_boolean ("pause", "Pause", "Pause", FALSE,
					G_PARAM_READABLE | G_PARAM_WRITABLE));
	
	signals[WATCHER_ADD] = g_signal_new ("watcher-add",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, watcher_add),
			NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE,
			1, G_TYPE_OBJECT);
	
	signals[WATCHER_UPDATE] = g_signal_new ("watcher-update",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, watcher_update),
			NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE,
			1, G_TYPE_OBJECT);
	
	signals[WATCHER_REMOVE] = g_signal_new ("watcher-remove",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, watcher_remove),
			NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE,
			1, G_TYPE_OBJECT);
	
	signals[WATCHER_ERROR] = g_signal_new ("watcher-error",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, watcher_error),
			NULL, NULL, gsm_marshal_VOID__OBJECT_POINTER, G_TYPE_NONE,
			2, G_TYPE_OBJECT, G_TYPE_POINTER);
	
	signals[FRIEND_ADD] = g_signal_new ("friend-add",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, friend_add),
			NULL, NULL, gsm_marshal_VOID__STRING_BOOL, G_TYPE_NONE,
			2, G_TYPE_STRING, G_TYPE_BOOLEAN);
	
	signals[FRIEND_UPDATE] = g_signal_new ("friend-update",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, friend_update),
			NULL, NULL, gsm_marshal_VOID__STRING_STRING_BOOL, G_TYPE_NONE,
			3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
	
	signals[FRIEND_REMOVE] = g_signal_new ("friend-remove",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, friend_remove),
			NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE,
			1, G_TYPE_STRING);
	
	signals[FRIEND_ONLINE] = g_signal_new ("friend-online",
			G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
			G_STRUCT_OFFSET (GsmModelClass, friend_online),
			NULL, NULL, gsm_marshal_VOID__STRING_OBJECT, G_TYPE_NONE,
			2, G_TYPE_STRING, G_TYPE_OBJECT);
}

static void
gsm_model_init (GsmModel* model)
{
	model->friends = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
}

GsmModel *
gsm_model_new ()
{
	return g_object_new (GSM_TYPE_MODEL, NULL);
}

static void
gsm_model_dispose (GObject *object)
{
	GsmModel *model = GSM_MODEL (object);
	
	GSList *iter;
	while ((iter = model->watchers))
		gsm_model_remove_server_real (model, iter->data);
	
	g_hash_table_unref (model->friends);
	
	if (G_OBJECT_CLASS (gsm_model_parent_class)->dispose)
		G_OBJECT_CLASS (gsm_model_parent_class)->dispose (object);
}

static void
gsm_model_get_property (GObject* object, guint prop_id, GValue* value,
		GParamSpec* pspec)
{
	GsmModel *model = GSM_MODEL (object);
	
	switch (prop_id) {
	case PROP_UPDATE_RATE:
		g_value_set_long (value, gsm_model_get_update_rate (model));
		break;
	case PROP_PAUSE:
		g_value_set_boolean (value, gsm_model_get_pause (model));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gsm_model_set_property (GObject* object, guint prop_id, const GValue* value,
		GParamSpec* pspec)
{
	GsmModel *model = GSM_MODEL (object);
	
	switch (prop_id) {
	case PROP_UPDATE_RATE:
		gsm_model_set_update_rate (model, g_value_get_long (value));
		break;
	case PROP_PAUSE:
		gsm_model_set_pause (model, g_value_get_boolean (value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static gboolean
gsm_model_timer_tick_cb (GsmModel *model)
{
	if (!model->watcher_iter)
		model->watcher_iter = model->watchers;
	
	if (model->watcher_iter && !model->pause) {
		gsm_watcher_update (model->watcher_iter->data);
		model->watcher_iter = g_slist_next (model->watcher_iter);
	}
	
	return TRUE;
}

static void
gsm_model_update_timer (GsmModel *model)
{
	if (model->sid)
		g_source_remove (model->sid);
	
	guint n = g_slist_length (model->watchers);
	if (n > 0)
		model->sid = g_timeout_add (model->rate / n,
				(GSourceFunc) gsm_model_timer_tick_cb, model);
}

void
gsm_model_set_update_rate (GsmModel *model, glong rate)
{
	g_return_if_fail (GSM_IS_MODEL (model));
	model->rate = rate;
	gsm_model_update_timer (model);
}

glong
gsm_model_get_update_rate (GsmModel *model)
{
	g_return_val_if_fail (GSM_IS_MODEL (model), 0);
	return model->rate;
}

void
gsm_model_set_pause (GsmModel *model, gboolean pause)
{
	g_return_if_fail (GSM_IS_MODEL (model));
	model->pause = pause;
}

gboolean
gsm_model_get_pause (GsmModel *model)
{
	g_return_val_if_fail (GSM_IS_MODEL (model), FALSE);
	return model->pause;
}

GsmWatcher *
gsm_model_find_by_address (GsmModel *model, const gchar *address)
{
	g_return_val_if_fail (GSM_IS_MODEL (model), NULL);
	
	GSList *iter = model->watchers;
	while (iter) {
		GsmWatcher *watcher = iter->data;
		if (!strcmp (watcher->address, address))
			return watcher;
		iter = g_slist_next (iter);
	}
	
	return NULL;
}


// Watchers   ==================================================================

static void
watcher_updated (GsmWatcher *watcher, GsmModel *model)
{
	g_signal_emit (model, signals[WATCHER_UPDATE], 0, watcher);
}

static void
watcher_error (GsmWatcher *watcher, GError *error, GsmModel *model)
{
	g_signal_emit (model, signals[WATCHER_ERROR], 0, watcher, error);
}

static void
watcher_player_online (GsmWatcher *watcher, GsmPlayer *player, GsmModel *model)
{
	gboolean notify;
	
	if (gsm_model_find_friend (model, player->name, &notify) && notify)
		g_signal_emit (model, signals[FRIEND_ONLINE], 0, player->name, player->watcher);
}

static void
watcher_player_offline (GsmWatcher *watcher, GsmPlayer *player, GsmModel *model)
{
	// FIXME: need optimization
	gboolean notify;
	GSList *l = watcher->players;
	while (l) {
		GsmPlayer *p = l->data;
		if (gsm_model_find_friend (model, p->name, &notify) && notify &&
				watcher->max_players - watcher->num_players == 1)
			g_signal_emit (model, signals[FRIEND_ONLINE], 0, p->name, p->watcher);
		l = g_slist_next (l);
	}
}

static void
gsm_model_save_servers (GsmModel *model)
{
	gint i = 1;
	GError *error = NULL;
	
	GKeyFile *kfile = g_key_file_new ();
	
	GSList *iter = model->watchers;
	while (iter) {
		GsmWatcher *watcher = iter->data;
		gchar group[16];
		g_sprintf (group, "Server %d", i);
		g_key_file_set_string (kfile, group, "Name", watcher->name);
		g_key_file_set_string (kfile, group, "Address", watcher->address);
		iter = g_slist_next (iter);
		i++;
	}
	
	gchar *data = g_key_file_to_data (kfile, NULL, NULL);
	g_key_file_free (kfile);
	gchar *path = g_build_filename (g_get_home_dir (), ".config", "gsmon",
			"servers.list", NULL);
	if (!g_file_set_contents (path, data, -1, &error)) {
		g_warning ("%s", error->message);
		g_error_free (error);
	}
	g_free (path);
	g_free (data);
}

static GsmWatcher *
gsm_model_add_server_real (GsmModel *model, const gchar *address, const gchar *name)
{
	GsmWatcher *watcher = gsm_watcher_source_new (address);
	g_signal_connect (watcher, "updated", G_CALLBACK (watcher_updated), model);
	g_signal_connect (watcher, "error", G_CALLBACK (watcher_error), model);
	g_signal_connect (watcher, "player-online", G_CALLBACK (watcher_player_online), model);
	g_signal_connect (watcher, "player-offline", G_CALLBACK (watcher_player_offline), model);
	strcpy (watcher->name, name ? name : address);
	model->watchers = g_slist_append (model->watchers, watcher);
	g_signal_emit (model, signals[WATCHER_ADD], 0, watcher);
	gsm_model_update_timer (model);
	return watcher;
}

GsmWatcher *
gsm_model_add_server (GsmModel *model, const gchar *address)
{
	GsmWatcher *watcher;
	watcher = gsm_model_add_server_real (model, address, NULL);
	gsm_model_save_servers (model);
	return watcher;
}

static void
gsm_model_remove_server_real (GsmModel *model, GsmWatcher *watcher)
{
	g_signal_emit (model, signals[WATCHER_REMOVE], 0, watcher);
	model->watchers = g_slist_remove (model->watchers, watcher);
	g_object_unref (watcher);
	gsm_model_update_timer (model);
}

void
gsm_model_remove_server (GsmModel *model, GsmWatcher *watcher)
{
	gsm_model_remove_server_real (model, watcher);
	gsm_model_save_servers (model);
}

void
gsm_model_load_servers (GsmModel *model)
{
	gchar *path = g_build_filename (g_get_home_dir (), ".config", "gsmon",
			"servers.list", NULL);
	GKeyFile *kfile = g_key_file_new ();
	if (!g_key_file_load_from_file (kfile, path, G_KEY_FILE_NONE, NULL)) {
		g_free (path);
		g_key_file_free (kfile);
		return;
	}
	g_free (path);
	
	gchar **groups, **group;
	groups = g_key_file_get_groups (kfile, NULL);
	for (group = groups; *group; group++) {
		gchar *name = g_key_file_get_string (kfile, *group, "Name", NULL);
		gchar *address = g_key_file_get_string (kfile, *group, "Address", NULL);
		
		gsm_model_add_server_real (model, address, name);
		
		g_free (name);
		g_free (address);
	}
	
	g_strfreev (groups);
	g_key_file_free (kfile);
}


// Friends   ===================================================================

static void
gsm_model_save_friends (GsmModel *model)
{
	GHashTableIter iter;
	GKeyFile *kfile;
	gpointer name, notify;
	gint i = 1;
	
	kfile = g_key_file_new ();
	
	g_hash_table_iter_init (&iter, model->friends);
	while (g_hash_table_iter_next (&iter, &name, &notify)) {
		gchar *group = g_strdup_printf ("Friend %d", i);
		g_key_file_set_string (kfile, group, "Name", (const gchar *)name);
		g_key_file_set_boolean (kfile, group, "Notify", GPOINTER_TO_INT (notify));
		g_free (group);
		i++;
	}
	
	gchar *path = g_build_filename (g_get_home_dir (), ".config", "gsmon",
			"friends.list", NULL);
	gsize length;
	gchar *data = g_key_file_to_data (kfile, &length, NULL);
	g_file_set_contents (path, data, length, NULL);
	g_free (data);
	g_free (path);
	
	g_key_file_free (kfile);
}

static void
gsm_model_add_friend_real (GsmModel *model, const gchar *name, gboolean notify)
{
	g_hash_table_insert (model->friends, g_strdup (name), GINT_TO_POINTER (notify));
	g_signal_emit (model, signals[FRIEND_ADD], 0, name, notify);
}

void
gsm_model_add_friend (GsmModel *model, const gchar *name, gboolean notify)
{
	gsm_model_add_friend_real (model, name, notify);
	gsm_model_save_friends (model);
}

void
gsm_model_update_friend	(GsmModel *model, const gchar *name, gboolean notify)
{
	g_hash_table_insert (model->friends, g_strdup (name), GINT_TO_POINTER (notify));
	g_signal_emit (model, signals[FRIEND_UPDATE], 0, name, NULL, notify);
	gsm_model_save_friends (model);
}

void
gsm_model_remove_friend (GsmModel *model, const gchar *name)
{
	g_hash_table_remove (model->friends, name);
	g_signal_emit (model, signals[FRIEND_REMOVE], 0, name);
	gsm_model_save_friends (model);
}

gboolean
gsm_model_find_friend (GsmModel *model, const gchar *name, gboolean *notify)
{
	if (!strcmp (name, ""))
		return FALSE;
	return g_hash_table_lookup_extended (model->friends, name, NULL,
			notify ? (gpointer *) notify : NULL);
}

void
gsm_model_load_friends (GsmModel *model)
{
	gchar *path = g_build_filename (g_get_home_dir (), ".config", "gsmon",
			"friends.list", NULL);
	GKeyFile *kfile = g_key_file_new ();
	if (!g_key_file_load_from_file (kfile, path, G_KEY_FILE_NONE, NULL)) {
		g_free (path);
		g_key_file_free (kfile);
		return;
	}
	g_free (path);
	
	gchar **groups, **group;
	
	groups = g_key_file_get_groups (kfile, NULL);
	for (group = groups; *group; group++) {
		gchar *name = g_key_file_get_string (kfile, *group, "Name", NULL);
		gboolean notify = g_key_file_get_boolean (kfile, *group, "Notify", NULL);
		
		gsm_model_add_friend_real (model, name, notify);
		
		g_free (name);
	}
	
	g_strfreev (groups);
	g_key_file_free (kfile);
}
